package de.saar.penguin.give2.server;

import java.util.*;

import give.world.*;
import give.util.GeometryTools;
import give.formula.*;
import java.util.ArrayList;

/**
 * The DescribedObject class is an abstraction layer to access some object
 * attributes easily (color, type ...) The class also contains a map of all
 * DescribedObjects.
 * 
 * @author Boris Fersing
 */
public class DescribedObject implements Comparable {

    private final World world;
    private final ObjectType type;
    private final String id;
    private final String room;
    private final ObjectColor color;
    private String rexp;
    private String spatialRE;

    public void setSpatialRE(String spatialRE) {
        this.spatialRE = spatialRE;
    }

    public String getSpatialRE() {
        return spatialRE;
    }
    private List<DescribedObject> roomContents = new ArrayList<DescribedObject>();

    public List<DescribedObject> getRoomContents() {
        return roomContents;
    }

    /**
     * Constructor, the DescribedObject will be put in the map after it has been
     * created.
     *
     * @param id
     *            The id of the object in the GIVE world from which the
     *            DescribedObject will be created.
     * @param world
     *            The world which contains the object corresponding to the id.
     */
    public DescribedObject(String id, World world) {
        // We need temp variables before assigning a value to the final
        // variables. Default value is NULL.
        ObjectType tempType = ObjectType.NULL;
        ObjectColor tempColor = ObjectColor.NULL;


        this.id = id;
        this.world = world;
        this.rexp = null;

        // Detect which type the object has in the GIVE world and assign
        // the corresponding ObjectType to the DescribedObject.
        for (ObjectType t : ObjectType.values()) {
            if (world.getType(id).equalsIgnoreCase(t.getPredicate())) {
                tempType = t;
                break;
            }
        }

        // Detect which color the object has in the GIVE world and assign
        // the corresponding ObjectColor to the DescribedObject.
        for (ObjectColor c : ObjectColor.values()) {
            // We don't assign a ObjectColor if the object has no color
            // attribute.
            if (world.getValueAsString(id, "color") != null
                    && world.getValueAsString(id, "color").equalsIgnoreCase(
                    c.getPredicate())) {
                tempColor = c;
                break;
            }
        }


        // The player can't see the difference between stateless and normal buttons
        // If something is a stateless button we define it as a normal button
        // to reflect the players vision.
        if (tempType == ObjectType.STATELESS_BUTTON) {
            tempType = ObjectType.BUTTON;
        }

        // Now assign the temp values to the final variables.
        this.type = tempType;
        this.color = tempColor;

        // Add information in which room the DescribedObject is located.
        // If this DescribedObject is a room itself, it per definition contains
        // itself. Otherwise the location will be looked up via the
        // world.GetAttributes method.
        if (!this.type.equals(ObjectType.ROOM)) {
            // Look up the room at the position of the object and assign the
            // room name to the object
            this.room = world.getValueAsString(id, "in-room");
        } else {
            this.room = this.id;
        }
    }

    /**
     * Computes the spatial relation between two objects. This will primarily be used
     * to compute the relation of a button towards a landmark.
     * @author Markus Draeger
     * @param obj the object that has a relation to a certain landmark
     * @param landmark the landmark relative to which the spatial relation will be computed
     * @param w the world in which both objects reside
     * @param p the position of the player
     * @return a String value representing the spatial relation. Can in theory directly be used to generate a RE
     */
    public static String computeSpatialRelation(String obj, String landmark, World w, Position p) {
        if (GIVEUtils.debug()) {
            System.out.println("\nCompute spatial relation between " + obj + " and " + landmark);
        }
        try {
            if (w.getType(obj) == null) {
                if (GIVEUtils.debug()) {
                    System.out.println("Object " + obj + " could not be found");
                }
                throw new InputMismatchException("Object " + obj + " could not be found");
            } else if (w.getType(landmark) == null) {
                if (GIVEUtils.debug()) {
                    System.out.println("Object " + landmark + " could not be found");
                }
                throw new InputMismatchException("Object " + obj + " could not be found");
            } else {
                // if both objects are (stateless-)buttons, player position is not needed. Thus I check for this in order to be able to reuse my "robust" code.
                if (w.getType(obj).contains("button") && w.getType(landmark).contains("button")) {
                    double or_y = w.getOrientation(obj).getZ();
                    double or_x = w.getOrientation(obj).getX();
                    double x_obj = w.getPosition(obj).getX();
                    double y_obj = w.getPosition(obj).getZ();
                    double x_landmark = w.getPosition(landmark).getX();
                    double y_landmark = w.getPosition(landmark).getZ();
                    double z_obj = w.getPosition(obj).getY();
                    double z_landmark = w.getPosition(landmark).getY();
                    //orientation points "downwards" in the mapviewer

                    if (or_y == -1.0) {
                        //System.out.println(obj +" points downwards in the mapviewer");
                        if (x_obj < x_landmark) {
                            return "right of";
                        } else if (x_obj > x_landmark) {
                            return "left of";
                        } // otherwise, things may be on top or beneath one another
                        else {
                            if (z_obj > z_landmark) {
                                return "above";
                            } else if (z_obj < z_landmark) {
                                return "below";
                            } else {
                                return "in the same place as";
                            }
                        }
                    } //orientation points "upwards" in the mapviewer
                    else if (or_y == 1.0) {
                        //		System.out.println(obj +" points upwards in the mapviewer");
                        if (x_obj < x_landmark) {
                            return "left of";
                        } else if (x_obj > x_landmark) {
                            return "right of";
                        } // otherwise, things may be on top or beneath one another
                        else {
                            if (z_obj > z_landmark) {
                                return "above";
                            } else if (z_obj < z_landmark) {
                                return "below";
                            } else {
                                return "in the same place as";
                            }
                        }
                    } // if orientation points "from left to right" in the mapviewer
                    else if (or_x == -1.0) {
                        if (y_obj > y_landmark) {
                            return "right of";
                        } else if (y_obj < y_landmark) {
                            return "left of";
                        } else {
                            if (z_obj > z_landmark) {
                                return "above";
                            } else if (z_obj < z_landmark) {
                                return "below";
                            } else {
                                return "in the same place as";
                            }
                        }
                    } // if orientation points "from right to left" in the mapviewer
                    else if (or_x == 1.0) {
                        if (y_obj > y_landmark) {
                            return "left of";
                        } else if (y_obj < y_landmark) {
                            return "right of";
                        } else {
                            if (z_obj > z_landmark) {
                                return "above";
                            } else if (z_obj < z_landmark) {
                                return "below";
                            } else {
                                return "in the same place as";
                            }
                        }
                    } else {
                        return "ERROR This should never happen: Please check computeSpatialRelation() Method";
                    }
                }//end if buttons

                // Player is at position p. If player would first look at the object, and then at the landmark, in which direction would he have to turn?

                // get Player coordinates
                double xP = p.getX();
                double yP = p.getZ();
                if (GIVEUtils.debug()) {
                    System.out.println("Player position is " + p.toString());
                }

                //
                // first we compute the player's orientation when looking at the landmark
                //

                // get landmark coordinates
                double xL = w.getPosition(landmark).getX();
                double yL = w.getPosition(landmark).getZ();
                // compute Distance from user to landmark on x-axis
                double xDist = xL - xP;
                if (GIVEUtils.debug()) {
                    System.out.println("x-axis-distance from player to " + landmark + " is: " + xDist);
                }
                // compute Distance from user to landmark on y-axis
                double yDist = yL - yP;
                if (GIVEUtils.debug()) {
                    System.out.println("y-axis-distance from player to " + landmark + " is: " + yDist);
                }
                Orientation playerLookAtLandmark = new Orientation(xDist, 0, yDist).normalize();
                if (GIVEUtils.debug()) {
                    System.out.println("Orientation vector from player to " + landmark + " is " + playerLookAtLandmark.toString());
                }

                //
                // now we compute the player's orientation when he looks at the object
                //

                // get object coordinates
                double xObj = w.getPosition(obj).getX();
                double yObj = w.getPosition(obj).getZ();
                // compute Distance from user to object on x-axis
                double xDist2 = xObj - xP;
                if (GIVEUtils.debug()) {
                    System.out.println("x-axis-distance from player to " + obj + " is: " + xDist2);
                }
                // compute Distance from user to object on y-axis
                double yDist2 = yObj - yP;
                if (GIVEUtils.debug()) {
                    System.out.println("y-axis-distance from player to " + obj + " is: " + yDist2);
                }
                Orientation playerLookAtObject = new Orientation(xDist2, 0, yDist2).normalize();
                if (GIVEUtils.debug()) {
                    System.out.println("Orientation vector from player to " + obj + " is " + playerLookAtObject.toString());
                }

                double angle = GeometryTools.getAngle(playerLookAtObject, playerLookAtLandmark);
                // angle is represented as a value between -Math.PI and +Math.PI
                if (angle >= (0 - Math.PI) && angle < 0) {
                    if (GIVEUtils.debug()) {
                        System.out.println(obj + " is right of " + landmark);
                    }
                    return "right of";
                } else if (angle >= 0 && angle <= Math.PI) {
                    if (GIVEUtils.debug()) {
                        System.out.println(obj + " is left of " + landmark);
                    }
                    return "left of";
                }
                return "ERROR2 This should never happen: Please check computeSpatialRelation() Method";
            }
        } catch (InputMismatchException e) {
            return (e.getMessage());
        }
    }
    // Various accessors

    /**
     * @return the id of the current object
     */
    public String getId() {
        return this.id;
    }

    /**
     * @return the type of the current object
     */
    public ObjectType getType() {
        return this.type;
    }

    /**
     * @return the color of the current object
     */
    public ObjectColor getColor() {
        return this.color;
    }

    /**
     * @return the room of the current object
     */
    public String getRoom() {
        return this.room;
    }

    /**
     * Checks if a room contains a given type of objects
     * @param objtype
     * @return true if the room contains an object of the type objtype, false otherwise
     */
    public boolean containsType(ObjectType objtype) {
        for (DescribedObject object : this.getRoomContents()) {
            if (object.getType() == objtype) {
                return true;
            }
        }
        return false;
    }

    public List<DescribedObject> getContentForType(ObjectType objtype) {
        List<DescribedObject> result = new ArrayList<DescribedObject>();
        for (DescribedObject object : this.getRoomContents()) {
            if (object.getType() == objtype) {
                result.add(object);
            }
        }
        return result;
    }

    /**
     * @return the world of the current object
     */
    public World getWorld() {
        return this.world;
    }

    public String toString() {
        String msg = "";
        if (this.color != ObjectColor.NULL) {
            msg = msg + this.color + " ";
        }

        return msg + this.type;
    }

    /**
     * @return the Referring Expression which corresponds to the object
     */
    public String getRexp() {
        if (rexp == null) {
            return "";
        } else {
            return rexp;
        }
    }

    /**
     * @param rexp
     * 			the referring expression which corresponds to the object
     */
    public void setRexp(String rexp) {
        this.rexp = rexp;
    }

    /**
     * This method initializes all the DescribedObjects. The method takes all
     * objects from the GIVE world and creates the corresponding
     * DescribedObjects.
     *
     * @param world
     *            The world which contains the objects from which we create
     *            DescribedObjects
     * @return void
     */
    public static Map<String, DescribedObject> initObjects(World world) {
        Map<String, DescribedObject> objects = new HashMap<String, DescribedObject>();
        for (Term t : world.getUniverseForTermType("object")) {

            // Add the DescribedObject to the Map if the entry doesn't already
            // exist.
            if (objects.get(t.toString()) == null) {
                objects.put(t.toString(), new DescribedObject(t.toString(), world));
                if (GIVEUtils.debug()) {
                    System.out.println("OBJECT " + t + " CREATED");
                }
            }
        }

        // Fill the room contents
        for (DescribedObject object : objects.values()) {
            if (object.getType() == ObjectType.ROOM) {
                // First add all elements returned by world's getRoomContents()
                for (String element : world.getRoomContents(object.getId())) {
                    object.roomContents.add(objects.get(element));
                }
            }
        }

        // Then search for missing doors and add them to roomContents
        for (DescribedObject object : objects.values()) {
            if (object.getType() == ObjectType.DOOR) {
                DescribedObject posroom = objects.get(world.getValueAsString(object.getId(), "positive-room"));
                DescribedObject negroom = objects.get(world.getValueAsString(object.getId(), "negative-room"));

                if (!posroom.roomContents.contains(object)) {
                    posroom.roomContents.add(object);
                }
                if (!negroom.roomContents.contains(object)) {
                    negroom.roomContents.add(object);
                }
            }
        }

        if (GIVEUtils.debug()) {
            System.out.println("!!! OBJECTS INIT : DONE !!!");
        }
        return objects;
    }

    public int compareTo(Object anotherDescObject) throws ClassCastException {
        if (!(anotherDescObject instanceof DescribedObject)) {
            throw new ClassCastException("A DescribedObject object expected.");
        }

        DescribedObject obj = (DescribedObject) anotherDescObject;
        return this.id.compareTo(obj.getId());
    }
}
